/*******************************************************************************
 * Copyright (c) 2002-2003 Boris Pruessmann and others.
 * All rights reserved.   This program and the accompanying materials
 * are made available under the terms of the Common Public License v0.5
 * which accompanies this distribution, and is available at
 * http://www.eclipse.org/legal/cpl-v05.html
 * 
 * Contributors:
 *    Boris Pruessmann - Initial implementation.
 ******************************************************************************/
package net.sourceforge.perforce.core.internal;

import java.io.File;
import java.io.IOException;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;

import jdbm.hash.HTree;
import jdbm.helper.MRU;
import jdbm.helper.ObjectCache;
import jdbm.recman.RecordManager;

import net.sourceforge.perforce.core.PerforceCorePlugin;
import net.sourceforge.perforce.core.api.IPerforceServer;
import net.sourceforge.perforce.core.syncinfo.FolderSyncInfo;
import net.sourceforge.perforce.core.syncinfo.ResourceSyncInfo;
import org.eclipse.core.internal.localstore.CoreFileSystemLibrary;
import org.eclipse.core.resources.IContainer;
import org.eclipse.core.resources.IProject;
import org.eclipse.core.resources.IResource;
import org.eclipse.core.runtime.IPath;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.Platform;
import org.eclipse.core.runtime.Status;

/**
  * @version $Revision: 244 $
  * @author  <a href="mailto:bpruessmann@users.sourceforge.net">Boris Pruessmann</a>
  */
public class CacheStore
{
	// --------------------------------------------------------------------- Private Static Constant
	
	/** The CacheStore current version. */
	private static final Integer CURRENT_VERSION = new Integer(20030303);
	
	// --------------------------------------------------------------------------- Private Variables
	
	/** Stores the server unique name. */
	private final String serverName;
	/** Stores the path of the cache store. */
	private IPath path; 
	/** Stores the Record Manager. */
	private RecordManager recordManager;
	/** Stores the Record Manager's cache. */
	private ObjectCache recordManagerCache;
	/** Stores the cache for ResourceSyncInfos. */
	private PersistentMap resourceSyncCache;
	/** Stores the cache for FolderSyncInfos. */
	private PersistentMap folderSyncCache;
	/** Stores the FolderSyncInfos for projects. */
	private PersistentMap projectFolderSyncCache;
	/** The status of the CacheStore. */
	private IStatus status = new Status(IStatus.OK, PerforceCorePlugin.ID, 0, "", null);
	/** Indicates whether we should output trace info. */
	private final boolean debug;

	// ------------------------------------------------------------------------- Public Constructors
	
	/** Constructor for CacheStore. 
	  * 
	  * @param server the perforce server.
	  */ 
	public CacheStore(IPerforceServer server)
	{
		serverName = server.getUniqueName();
		final String value = Platform.getDebugOption(PerforceCorePlugin.ID + "/cache");//$NON-NLS-1$
		debug = PerforceCorePlugin.getPlugin().isDebugging() && 
		        Boolean.valueOf(value).booleanValue();
		
		try
		{
			String filename = 
				server.getPort() + '_' + server.getClient() + '_' + server.getUser();
			filename = filename.replace(':', '_');
			
			path = PerforceCorePlugin.getPlugin().getStateLocation().append(filename);
			if (debug)
			{
				System.out.println();
				System.out.println("CacheStore for " + serverName);                    //$NON-NLS-1$
				System.out.println("\t" + path.toOSString());                          //$NON-NLS-1$
			}
			
			recordManager = new RecordManager(path.toOSString());
			recordManagerCache = new ObjectCache(recordManager, new MRU(5));
			
			PersistentMap info = getPersistentMap("info");
			Integer version = (Integer) info.get("version");
			if (version == null || !version.equals(CacheStore.CURRENT_VERSION))
			{
				recordManager.close();
				recordManagerCache.dispose();
				
				status = 
					new Status(
						IStatus.INFO, 
						PerforceCorePlugin.ID, 
						0,
						"The cache store " + filename + " was deleted due to a version conflict.", 
						null); 
						
				
				File cacheFile = new File(path.toOSString() + ".db");
				cacheFile.delete();
					
				cacheFile = new File(path.toOSString() + ".lg");
				cacheFile.delete();
				
				recordManager = new RecordManager(path.toOSString());
				recordManagerCache = new ObjectCache(recordManager, new MRU(5));

				info = getPersistentMap("info");
				info.put("version", CacheStore.CURRENT_VERSION);
				info.dispose();
			}
							
			resourceSyncCache = getPersistentMap("resourceSyncCache");
			folderSyncCache = getPersistentMap("folderSyncCache");
			projectFolderSyncCache = getPersistentMap("projectFolderSyncCache");
		}
		catch (IOException e)
		{
			status = 
				new Status(
					IStatus.ERROR, PerforceCorePlugin.ID, 0,
					"An I/O error occured opening the cache store.", e); 

			PerforceCorePlugin.logError(e.getMessage(), e);
		}
	}
	
	// ------------------------------------------------------------------------------ Public Methods

	/** Returns the status of the CacheStore.
	  *
	  * @return the CacheStore's status. 
	  */
	public IStatus getStatus()
	{
		return status;
	}
	
	/** Call this method to check whether the cache has any pending changes.
	  * 
	  * @return true if there are changes, false otherwise.
	  */
	public boolean isDirty()
	{
		return 
			projectFolderSyncCache.isDirty() || 
			folderSyncCache.isDirty() ||
			resourceSyncCache .isDirty();
	}

	/** Called to commit the CacheStore's dirty data.
	  * 
	  * @throws IOException in case of an I/O error.
	  */
	public void commit()
		throws IOException
	{
		if (!isDirty())
		{
			return;
		}
		
		if (debug)
		{
			System.out.println();
			System.out.print("Started committing CacheStore for " + serverName);       //$NON-NLS-1$
			System.out.println(" " + new Date());                                      //$NON-NLS-1$
		}
		
		projectFolderSyncCache.commit();
		folderSyncCache.commit();
		resourceSyncCache.commit();
		
		if (debug)
		{
			System.out.print("Finished committing CacheStore for " + serverName);      //$NON-NLS-1$
			System.out.println(" " + new Date());                                      //$NON-NLS-1$
		}
	}

	/** Call this method to close the CacheStore. 
	  * 
	  * @throws IOException in case of an I/O error
	  */
	public void close()
		throws IOException
	{
		commit();
		
		projectFolderSyncCache.dispose();
		folderSyncCache.dispose();
		resourceSyncCache.dispose();
		
		recordManager.close();
		recordManagerCache.dispose();
	}
	
	/** Call this method to remove the CacheStore from the system. */
	public void delete()		
	{
		File dbFile = new File(path.toOSString() + ".db");                             //$NON-NLS-1$
		dbFile.delete();
		
		File lgFile = new File(path.toOSString() + ".lg");                             //$NON-NLS-1$
		lgFile.delete();
	}

	/** Gets the project's FolderSyncInfo.
	  * 
	  * @param project the project.
	  * @return the FolderSyncInfo.
	  */
	public FolderSyncInfo getProjectSyncInfo(IProject project)
	{
		try
		{
			String key = createCacheKeyFromPath(project.getLocation());
			return (FolderSyncInfo) projectFolderSyncCache.get(key);
		}
		catch (IOException e)
		{
			PerforceCorePlugin.logError(e.getMessage(), e);
		}
		
		return null;
	}

	/** Gets FolderSyncInfo from cache.
	  * 
	  * @param container the container for which we need the FolderSyncInfo.
	  * @return the folder sync info or null.
	  */	
	public FolderSyncInfo[] getFolderSyncInfo(IContainer container)
	{
		try
		{
			String key = createCacheKeyFromPath(container.getLocation());
			Map children = (Map) folderSyncCache.get(key);
			if (children != null)
			{
				return (FolderSyncInfo[]) children.values().toArray(
					new FolderSyncInfo[children.size()]);
			}
		}
		catch (IOException e)
		{
			PerforceCorePlugin.logError(e.getMessage(), e);
		}
		
		return new FolderSyncInfo[0];	
	}
	
	/** Persistently stores the specified FolderSyncInfo for the given project.
	  *
	  * @param project the project.
	  * @param syncInfo the FolderSyncInfo to store. 
	  * @return the previously stored FolderSyncInfo.
	  */
	public FolderSyncInfo setProjectSyncInfo(IProject project, FolderSyncInfo syncInfo)
	{		
		try
		{
			FolderSyncInfo result = null;
			String key = createCacheKeyFromPath(project.getLocation());
			result = (FolderSyncInfo) projectFolderSyncCache.get(key);
			if (syncInfo == null)
			{
				projectFolderSyncCache.remove(key);
			}
			else
			{
				projectFolderSyncCache.put(key, syncInfo);
			}
			
			return result;
		}
		catch (IOException e)
		{
			PerforceCorePlugin.logError(e.getMessage(), e);
			return null;
		}
	}
	
	/** Persistently stores the specified FolderSyncInfo for the given container.
	  *
	  * @param container the container.
	  * @param syncInfo the FolderSyncInfo to store. 
	  * @return the previously stored FolderSyncInfo.
	  */
	public FolderSyncInfo setFolderSyncInfo(IContainer container, FolderSyncInfo syncInfo)
	{
		FolderSyncInfo result = null;
		try
		{
			IContainer parent = container.getParent();
			String key = createCacheKeyFromPath(parent.getLocation());
			Map children = (Map) folderSyncCache.get(key);
			if (children != null)
			{
				result = (FolderSyncInfo) children.get(container.getName());
				if (syncInfo == null)
				{
					children.remove(container.getName());
					if (children.size() == 0)
					{
						folderSyncCache.remove(key);
					}
					else
					{
						folderSyncCache.put(key, children);
					}
				}
				else
				{
					if (!syncInfo.equals(result))
					{
						children.put(container.getName(), syncInfo);
						folderSyncCache.put(key, children);
					}
				}
			}
			else
			{
				children = new HashMap();
				if (syncInfo != null)
				{
					children.put(container.getName(), syncInfo);
				}
				folderSyncCache.put(key, children);
			}
			
			return result;
		}
		catch (IOException e)
		{
			PerforceCorePlugin.logError(e.getMessage(), e);
			return null;
		}
	}
	
	/** Wrapper for fstat().
	  * 
	  * @param container that contains resources for which we need ResourceSyncInfo's.
	  * @return array of ResourceSyncInfo.
	  */		
	public ResourceSyncInfo[] getResourceSyncInfo(IContainer container)
	{
		try
		{
			String key = createCacheKeyFromPath(container.getLocation());
			Map children = (Map) resourceSyncCache.get(key);
			if (children != null)
			{
				return (ResourceSyncInfo[]) children.values().toArray(
					new ResourceSyncInfo[children.size()]);
			}
		}
		catch (IOException e)
		{
			PerforceCorePlugin.logError("CacheStore.getResourceSyncInfo failed.", e);  //$NON-NLS-1$
		}
		
		return new ResourceSyncInfo[0];		
	}	

	/** Puts ResourceSyncInfos into the cache.
	 * 
	 * @param resource the resource.
	 * @param syncInfo the associated ResourceSyncInfo.
	 * @return old sync info.	 
	 */
	public ResourceSyncInfo setResourceSyncInfo(IResource resource, ResourceSyncInfo syncInfo) 
	{		
		ResourceSyncInfo result = null;
		try
		{
			IContainer parent = resource.getParent();
			String key = createCacheKeyFromPath(parent.getLocation());
			Map children = (Map) resourceSyncCache.get(key);
			if (children != null)
			{
				result = (ResourceSyncInfo) children.get(resource.getName());
				if (syncInfo == null)
				{
					children.remove(resource.getName());
					if (children.size() == 0)
					{
						resourceSyncCache.remove(key);
					}
					else
					{
						resourceSyncCache.put(key, children);
					}
				}
				else
				{
					if (!syncInfo.equals(result))
					{
						children.put(resource.getName(), syncInfo);
						resourceSyncCache.put(key, children);
					}					
				}
			}
			else
			{
				children = new HashMap();
				if (syncInfo != null)
				{
					children.put(resource.getName(), syncInfo);
				}
				resourceSyncCache.put(key, children);
			}
			
			return result;
		}
		catch (IOException e)
		{
			PerforceCorePlugin.logError("CacheStore.setResourceSyncInfo failed.", e);  //$NON-NLS-1$
			return null;
		}
	}

	// ----------------------------------------------------------------------------- Private Methods

	/** Creates the (resource sync) cache key from the given path.
	  * 
	  * @param path the path.
	  * @return the cache key.
	  */
	private String createCacheKeyFromPath(IPath path)
	{
		if (CoreFileSystemLibrary.isCaseSensitive())
		{
			return path.toOSString();
		}
		else
		{
			return path.toOSString().toLowerCase();
		}
	}

	/** Called to retrieve the PersistentMap with the given name.
	  * 
	  * @param name the name of the map.
	  * @return the PeristenMap.
	  * @throws IOException in case of an unexpected I/O error.
	  */
    private PersistentMap getPersistentMap(String name)
	    throws IOException 
	{
        long recordId = recordManager.getNamedObject(name);
        if (recordId == 0) 
        {
            HTree tree = new HTree(recordManager, recordManagerCache);
            recordId = tree.getRecid();
            recordManager.setNamedObject(name, recordId);
            recordManager.commit();
        }
        
        return new PersistentMap(recordManager, recordManagerCache, recordId);
    }

	// ---------------------------------------------------------------------- Private Nested Classes
	
	/** TODO: Convert HTree to BTree */
	private static class PersistentMap 
	{
		// ----------------------------------------------------------------------- Private Variables
		
		/** Stores the RecordManager instance. */
	    private RecordManager recordManager;
	    /** Stores the HTree instance. */
	    private HTree tree;
	    /** Indicates whether the map is dirty or not. */
	    private boolean isDirty;
	
		// --------------------------------------------------------------------- Public Constructors
	
		/** Constructor for PersistentMap. 
		  *
		  * @param theRecordManager the RecordManager
		  * @param cache the ObjectCache.
		  * @param recordId the root id.
		  * @throws IOException in case of an unexpected I/O error.
		  */
	    public PersistentMap(RecordManager theRecordManager, ObjectCache cache, long recordId)
	    	throws IOException 
	    {
	        recordManager = theRecordManager;
	        tree = HTree.load(theRecordManager, cache, recordId);
	    }	
	    
	    // ------------------------------------------------------------------------- Public Methods
	
		/** Checkes whether the persistent map is dirty or not.
		  *
		  * @return true or false. 
		  */
		public boolean isDirty()
		{
			return isDirty;
		}
	
	    /**
	      * Associates the specified value with the specified key.
	      *
	      * @param key key with which the specified value is to be assocated.
	      * @param value value to be associated with the specified key.
	      * @throws IOException in case of an I/O Error.
	      */
	    public void put(Object key, Object value) 
	    	throws IOException
	    {	    	
        	tree.put(key, value);
        	isDirty = true;			
	    }
		
	    /**
	      * Returns the value which is associated with the given key. Returns
	      * <code>null</code> if there is not association for this key.
	      *
	      * @param key key whose associated value is to be returned
	      * @return the associated object.
	      * @throws IOException in case of an I/O Error.
	      */
	    public Object get(Object key) 
	    	throws IOException 
	    {
	        return tree.get(key);	        
	    }
	
	    /**
	      * Remove the value which is associated with the given key.  If the key
	      * does not exist, this method simply ignores the operation.
	      *
	      * @param key key whose associated value is to be removed
	      * @throws IOException in case of an I/O Error.
	      */
	    public void remove(Object key) 
	    	throws IOException 
	    {	    	
	        tree.remove(key);
			isDirty = true;
	    }
	
		/** Commit dirty data.
		  * 
		  * @throws IOException in case of an I/O Error.
		  */
		public void commit()
			throws IOException
		{
			if (isDirty)
			{
				recordManager.commit();
				isDirty = false;
			}
		}
	
	    /**
	      * Disposes from this hashtable instance and frees associated
	      * resources.
	      * 
	      * @throws IOException in case of an I/O Error.
	      */
	    public void dispose() 
	    	throws IOException 
		{
			recordManager.commit();
	        tree.dispose();
	        
	        tree = null;
	        recordManager = null;
	    }	
	}
}